﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using TheDealmap.Data;
using TheDealmap.Enum;
using TheDealmap.Util;
using System.Web;


namespace TheDealmap
{
    public class Service
    {
        /// <summary>
        /// The Dealmap url to direct deal search requests to.
        /// </summary>
        private const string SEARCH_DEALS = "http://api.thedealmap.com/search/deals/";

        /// <summary>
        /// The Dealmap url to direct business search requests to.
        /// </summary>
        private const string SEARCH_BUSINESSES = "http://api.thedealmap.com/search/businesses/";

        /// <summary>
        /// The Dealmap url to submit new deals to.
        /// </summary>
        private const string ADD_DEALS = "http://api.thedealmap.com/deals/";

        /// <summary>
        /// Internal holder for the apikey used to authenticate to the dealmap api.
        /// </summary>
        private string _apikey = "";

        /// <summary>
        /// Gets the Dealmap Api developer key that is used to make requests to the Dealmap api.
        /// </summary>
        /// <value>The API key.</value>
        public string ApiKey
        {
            get
            {
                return _apikey;
            }
            set
            {
                _apikey = value;
            }
        }

        /// <summary>
        /// Private default constructor prevents a dealmap being created without an apikey.
        /// </summary>
        private Service() { }

        /// <summary>
        /// Constructs a DealMap instance setting the users Api Key.
        /// </summary>
        /// <param name="apikey"></param>
        /// <remarks>If this has an optional parameter then the above private constructor will not hide the default constructor</remarks>
        public Service(string apikey)
        {
            this._apikey = apikey;
        }

        /// <summary>
        /// Searches the deals.
        /// </summary>
        /// <param name="activity">A CenterdActivity or bitwise joined collection of CenterdActivities.</param>
        /// <param name="capability">A Capability or bitwise joined collection of Capabilities.</param>
        /// <param name="expirationDate">The date when the deal is set to expire. eg. DateTime.Now.AddDays(3)</param>
        /// <param name="location">The location of the deal in a format that can be geocoded eg.<example>"Seatle, WA" "Huston, TX" "Redmond, WA"</example>.</param>
        /// <param name="query">Optional. Additional text to search for a deal by <example>"pizza" "fun" "Something light hearted"</example></param>
        /// <param name="distance">Optional. The distance from the resolved geo location to search for deals in miles. eg.<example>5.0</example><example>10.354</example></param>
        /// <param name="startIndex">Optional. The first index of the matched results to return.</param>
        /// <param name="pageSize">Optional. The number of results to return.</param>
        /// <returns>A Deals object that contains the total number of matching Deals from the query as well as the number of deals defined by the pageSize</returns>
        private Deals SearchDeals(
            CenterdActivity activity,
            Capability capability,
            DateTime expirationDate,
            string location,
            string query,
            double? distance,
            int? startIndex,
            int? pageSize)
        {
            return SearchDeals(this._apikey, activity, capability, expirationDate, location, query, distance, startIndex, pageSize);
        }

        /// <summary>
        /// Searches the deals using a already geocoded latitude and longitude.
        /// </summary>
        /// <param name="activity">A CenterdActivity or bitwise joined collection of CenterdActivities.</param>
        /// <param name="capability">A Capability or bitwise joined collection of Capabilities.</param>
        /// <param name="expirationDate">The date when the deal is set to expire. eg. DateTime.Now.AddDays(3)</param>
        /// <param name="geoLatitude">The geo code latitude that defines part of the centre of the search location. Where the value is a double &gt; -90.00 and &lt; 90.00</param>
        /// <param name="geoLongitude">The geo code lonitude that defines part of the centre of the search location. Where the value is a double &gt; -180.00 and &lt; 180.00</param>
        /// <param name="query">Optional. Additional text to search for a deal by <example>"pizza" "fun" "Something light hearted"</example></param>
        /// <param name="distance">Optional. The distance from the resolved geo location to search for deals in miles. eg.<example>5.0</example><example>10.354</example></param>
        /// <param name="startIndex">Optional. The first index of the matched results to return.</param>
        /// <param name="pageSize">Optional. The number of results to return. Default: 20 Max: 100 .</param>
        /// <returns>A Deals object that contains the total number of matching Deals from the query as well as the number of deals defined by the pageSize</returns>
        public Deals SearchDeals(
            CenterdActivity activity,
            Capability capability,
            DateTime expirationDate,
            double geoLatitude,
            double geoLongitude,
            string query,
            double? distance,
            int? startIndex,
            int? pageSize)
        {
            GeoCoord latlong = new GeoCoord(geoLatitude, geoLongitude);
            return SearchDeals(this._apikey, activity, capability, expirationDate, latlong.ToString(), query, distance, startIndex, pageSize);
        }

        /// <summary>
        /// Searches the deals.
        /// </summary>
        /// <param name="apikey">The apikey.</param>
        /// <param name="activity">A CenterdActivity or bitwise joined collection of CenterdActivities.</param>
        /// <param name="capability">A Capability or bitwise joined collection of Capabilities.</param>
        /// <param name="expirationDate">The date when the deal is set to expire. eg. DateTime.Now.AddDays(3)</param>
        /// <param name="location">The location of the deal in a format that can be geocoded eg.<example>"Seatle, WA" "Huston, TX" "Redmond, WA"</example>.</param>
        /// <param name="query">Optional. Additional text to search for a deal by <example>"pizza" "fun" "Something light hearted"</example></param>
        /// <param name="distance">Optional. The distance from the resolved geo location to search for deals in miles. eg.<example>5.0</example><example>10.354</example></param>
        /// <param name="startIndex">Optional. The first index of the matched results to return.</param>
        /// <param name="pageSize">Optional. The number of results to return.</param>
        /// <returns>A Deals object that contains the total number of matching Deals from the query as well as the number of deals defined by the pageSize</returns>
        private static Deals SearchDeals(
            string apikey,
            CenterdActivity activity,
            Capability capability,
            DateTime expirationDate,
            string location,
            string query,
            double? distance,
            int? startIndex,
            int? pageSize
            )
        {
            //string uri = BuildGetUri(
            //SEARCH_DEALS,
            //apikey,
            //location: location,
            //query: query,
            //distance: distance,
            //startIndex: startIndex,
            //pageSize: pageSize,
            //activity: activity,
            //capability: capability,
            //expirationDate: expirationDate
            //);
            string uri = new SearchBuilder(apikey, SEARCH_DEALS)
                .Location(location)
                .Query(query)
                .Distance(distance)
                .StartIndex(startIndex)
                .PageSize(pageSize)
                .Activity(activity)
                .Capability(capability)
                .ExpirationDate(expirationDate)
                .Create();

            var response = GetHTTPResponse(uri);
            var deals = DealSerializer.DeSerialize<Deals>(response);
            return deals;
        }

        /// <summary>
        /// Searches the deals.
        /// </summary>
        /// <param name="apikey">The apikey.</param>
        /// <param name="activity">A CenterdActivity or bitwise joined collection of CenterdActivities.</param>
        /// <param name="capability">A Capability or bitwise joined collection of Capabilities.</param>
        /// <param name="expirationDate">The date when the deal is set to expire. eg. DateTime.Now.AddDays(3)</param>
        /// <param name="geoLatitude">The geo code latitude that defines part of the centre of the search location. Where the value is a double &gt; -90.00 and &lt; 90.00</param>
        /// <param name="geoLongitude">The geo code lonitude that defines part of the centre of the search location. Where the value is a double &gt; -180.00 and &lt; 180.00</param>
        /// <param name="query">Optional. Additional text to search for a deal by <example>"pizza" "fun" "Something light hearted"</example></param>
        /// <param name="distance">Optional. The distance from the resolved geo location to search for deals in miles. eg.<example>5.0</example><example>10.354</example></param>
        /// <param name="startIndex">Optional. The first index of the matched results to return.</param>
        /// <param name="pageSize">Optional. The number of results to return. Default: 20 Max: 100 .</param>
        /// <returns>A Deals object that contains the total number of matching Deals from the query as well as the number of deals defined by the pageSize</returns>
        public static Deals SearchDeals(
            string apikey,
            CenterdActivity activity,
            Capability capability,
            DateTime expirationDate,
            double geoLatitude,
            double geoLongitude,
            string query,
            double? distance,
            int? startIndex,
            int? pageSize)
        {
            GeoCoord latlong = new GeoCoord(geoLatitude, geoLongitude);
            return SearchDeals(apikey, activity, capability, expirationDate, latlong.ToString(), query, distance, startIndex, pageSize);
        }

        /// <summary>
        /// Searches the business listings.
        /// </summary>
        /// <param name="activity">A CenterdActivity or bitwise joined collection of CenterdActivities.</param>
        /// <param name="location">The location of the business in a format that can be geocoded eg.<example>"Seatle, WA" "Huston, TX" "Redmond, WA"</example>.</param>
        /// <param name="query">Optional. Additional text to search for a business by <example>"pizza" "fun" "Something light hearted"</example></param>
        /// <param name="distance">Optional. The distance from the resolved geo location to search for businesses in miles. eg.<example>5.0</example><example>10.354</example></param>
        /// <param name="startIndex">Optional. The first index of the matched results to return.</param>
        /// <param name="pageSize">Optional. The number of results to return. Default: 20 Max: 100 .</param>
        /// <returns>A Businesses object that contains the total number of matching Businesses from the query as well as the number of businesses defined by the pageSize</returns>
        private Businesses SearchBusinessListings(
            CenterdActivity activity,
            string location,
            string query,
            double? distance,
            int? startIndex,
            int? pageSize)
        {
            return SearchBusinessListings(this._apikey, activity, location, query, distance, startIndex, pageSize);
        }

        /// <summary>
        /// Searches the business listings.
        /// </summary>
        /// <param name="activity">A CenterdActivity or bitwise joined collection of CenterdActivities.</param>
        /// <param name="geoLatitude">The geo code latitude that defines part of the centre of the search location. Where the value is a double &gt; -90.00 and &lt; 90.00</param>
        /// <param name="geoLongitude">The geo code lonitude that defines part of the centre of the search location. Where the value is a double &gt; -180.00 and &lt; 180.00</param>
        /// <param name="query">Optional. Additional text to search for a business by <example>"pizza" "fun" "Something light hearted"</example></param>
        /// <param name="distance">Optional. The distance from the resolved geo location to search for businesses in miles. eg.<example>5.0</example><example>10.354</example></param>
        /// <param name="startIndex">Optional. The first index of the matched results to return.</param>
        /// <param name="pageSize">Optional. The number of results to return. Default: 20 Max: 100 .</param>
        /// <returns>A Businesses object that contains the total number of matching Businesses from the query as well as the number of businesses defined by the pageSize</returns>
        public Businesses SearchBusinessListings(
            CenterdActivity activity,
            double geoLatitude,
            double geoLongitude,
            string query,
            double? distance,
            int? startIndex,
            int? pageSize)
        {
            GeoCoord latlong = new GeoCoord(geoLatitude, geoLongitude);
            return SearchBusinessListings(this._apikey, activity, latlong.ToString(), query, distance, startIndex, pageSize);
        }

        /// <summary>
        /// Searches the business listings.
        /// </summary>
        /// <param name="apikey">The apikey.</param>
        /// <param name="activity">A CenterdActivity or bitwise joined collection of CenterdActivities.</param>
        /// <param name="location">The location of the business in a format that can be geocoded eg.<example>"Seatle, WA" "Huston, TX" "Redmond, WA"</example>.</param>
        /// <param name="query">Optional. Additional text to search for a business by <example>"pizza" "fun" "Something light hearted"</example></param>
        /// <param name="distance">Optional. The distance from the resolved geo location to search for businesses in miles. eg.<example>5.0</example><example>10.354</example></param>
        /// <param name="startIndex">Optional. The first index of the matched results to return.</param>
        /// <param name="pageSize">Optional. The number of results to return. Default: 20 Max: 100 .</param>
        /// <returns>A Businesses object that contains the total number of matching Businesses from the query as well as the number of businesses defined by the pageSize</returns>
        private static Businesses SearchBusinessListings(
            string apikey,
            CenterdActivity activity,
            string location,
            string query,
            double? distance,
            int? startIndex,
            int? pageSize)
        {
            //string uri = BuildGetUri(
            //  SEARCH_BUSINESSES,
            //  apikey,
            //  location: location,
            //  query: query,
            //  distance: distance,
            //  startIndex: startIndex,
            //  pageSize: pageSize,
            //  activity: activity);

            string uri =
                new SearchBuilder(apikey, SEARCH_BUSINESSES)
                .Location(location)
                .Query(query)
                .Distance(distance)
                .StartIndex(startIndex)
                .PageSize(pageSize)
                .Activity(activity)
                .Create();
            var response = GetHTTPResponse(uri);
            var busineses = DealSerializer.DeSerialize<Businesses>(response);
            return busineses;
        }

        /// <summary>
        /// Searches the business listings using a provided apikey.
        /// </summary>
        /// <param name="apikey">The apikey.</param>
        /// <param name="activity">A CenterdActivity or bitwise joined collection of CenterdActivities.</param>
        /// <param name="geoLatitude">The geo code latitude that defines part of the centre of the search location. Where the value is a double &gt; -90.00 and &lt; 90.00</param>
        /// <param name="geoLongitude">The geo code lonitude that defines part of the centre of the search location. Where the value is a double &gt; -180.00 and &lt; 180.00</param>
        /// <param name="query">Optional. Additional text to search for a business by <example>"pizza" "fun" "Something light hearted"</example></param>
        /// <param name="distance">Optional. The distance from the resolved geo location to search for businesses in miles. eg.<example>5.0</example><example>10.354</example></param>
        /// <param name="startIndex">Optional. The first index of the matched results to return.</param>
        /// <param name="pageSize">Optional. The number of results to return. Default: 20 Max: 100 .</param>
        /// <returns>A Businesses object that contains the total number of matching Businesses from the query as well as the number of businesses defined by the pageSize</returns>
        public static Businesses SearchBusinessListings(
            string apikey,
            CenterdActivity activity,
            double geoLatitude,
            double geoLongitude,
            string query,
            double? distance,
            int? startIndex,
            int? pageSize)
        {
            GeoCoord latlong = new GeoCoord(geoLatitude, geoLongitude);
            return SearchBusinessListings(apikey, activity, latlong.ToString(), query, distance, startIndex, pageSize);
        }

        /// <summary>
        /// This method allows you to to get the deal details such as deal description, 
        /// terms/restrictions, business description as well as the basic data that 
        /// you get in a typical search method. 
        /// </summary>
        /// <param name="dealId">The Identifier if the deal you are looking for.</param>
        /// <returns></returns>
        public Deal GetDealDetails(string dealId)
        {
            return GetDealDetails(this._apikey, dealId);
        }

        /// <summary>
        /// This method allows you to to get the deal details such as deal description, 
        /// terms/restrictions, business description as well as the basic data that 
        /// you get in a typical search method. 
        /// </summary>
        /// <param name="dealId">The Identifier if the deal you are looking for.</param>
        /// <returns></returns>
        public static Deal GetDealDetails(string apikey, string dealId)
        {
            var uri = ADD_DEALS + dealId + "?key=" + apikey;
            var res = GetHTTPResponse(uri);
            var deal = DealSerializer.DeSerialize<Deal>(res);
            return deal;
        }

        /// <summary>
        /// Submits  new deal to the Dealmap api.
        /// </summary>
        /// <param name="newDeal">The new deal.</param>
        /// <param name="apikey">The apikey to make the deal request with</param>
        /// <returns>The response from the deal request.</returns>
        public static AddDealResponse SubmitDeal(string apikey, ProposedDeal newDeal)
        {
            var xml = DealSerializer.Serialize<ProposedDeal>(newDeal);
            var resp = PostHttpRequest(ADD_DEALS, xml);
            return DealSerializer.DeSerialize<AddDealResponse>(resp);
        }

        /// <summary>
        /// Submits  new deal to the Dealmap api.
        /// </summary>
        /// <param name="newDeal">The new deal.</param>
        /// <returns>The response from the deal request.</returns>
        public AddDealResponse SubmitDeal( ProposedDeal newDeal)
        {
            return SubmitDeal(this._apikey, newDeal);
        }

        /// <summary>
        /// Posts the HTTP request.
        /// </summary>
        /// <param name="uri">The URI.</param>
        /// <param name="requestBody">The request body.</param>
        /// <returns></returns>
        private static string PostHttpRequest(string uri, string requestBody)
        {
            HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri);
            req.AllowAutoRedirect = true;
            req.ContentType = "application/xml; charset=utf-8";
            byte[] arrBytes = Encoding.UTF8.GetBytes(requestBody);
            System.Net.ServicePointManager.Expect100Continue = false;
            req.KeepAlive = true;
            req.Accept = "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2";
            req.ContentLength = arrBytes.Length;
            req.Method = "POST";

            //turn off Expect-100


            using (Stream stream = req.GetRequestStream())
            {
                stream.Write(arrBytes, 0, arrBytes.Length);
                stream.Flush();
                stream.Close();
            }

            var resp = (HttpWebResponse)req.GetResponse();
            string content;
            using (StreamReader streamReader = new StreamReader(resp.GetResponseStream()))
            {
                content = streamReader.ReadToEnd();
            }
            //return ((int)resp.StatusCode == 200);
            return content;
        }

        /// <summary>
        /// Gets the HTTP response.
        /// </summary>
        /// <param name="uri">The URI.</param>
        /// <returns></returns>
        private static string GetHTTPResponse(string uri)
        {
            Debug.WriteLine(uri);

            string response = "";
            HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(uri);
            httpWebRequest.Method = "GET";
            HttpWebResponse resp = (HttpWebResponse)httpWebRequest.GetResponse();
            using (Stream strm = resp.GetResponseStream())
            {
                using (StreamReader strmRead = new StreamReader(strm))
                    response = strmRead.ReadToEnd();
            }

            return response;
        }

        ///// <summary>
        ///// Builds the get URI.
        ///// </summary>
        ///// <param name="uri">The URI.</param>
        ///// <param name="apikey">The apikey.</param>
        ///// <param name="activity">The activity.</param>
        ///// <param name="capability">The capability.</param>
        ///// <param name="expirationDate">The expiration date.</param>
        ///// <param name="location">The location.</param>
        ///// <param name="query">The query.</param>
        ///// <param name="distance">The distance.</param>
        ///// <param name="startIndex">The start index.</param>
        ///// <param name="pageSize">Size of the page.</param>
        ///// <returns></returns>
        //private static string BuildGetUri(
        //    string uri,
        //    string apikey,
        //    CenterdActivity? activity = null,
        //    Capability? capability = null,
        //    DateTime? expirationDate = null,
        //    string location = null,
        //    string query = null,
        //    double? distance = null,
        //    int? startIndex = null,
        //    int? pageSize = null)
        //{
        //    NameValueCollection parameters = new NameValueCollection();

        //    if (location != null) parameters.Add("l", location);
        //    if (query != null) parameters.Add("q", query);
        //    if (distance.HasValue) parameters.Add("d", distance.ToString());
        //    if (startIndex.HasValue) parameters.Add("si", startIndex.ToString());
        //    if (pageSize.HasValue) parameters.Add("ps", pageSize.ToString());
        //    if (activity.HasValue) parameters.Add("a", ((int)activity).ToString());
        //    if (capability.HasValue) parameters.Add("c", ((int)capability).ToString());
        //    if (expirationDate.HasValue) parameters.Add("ed", expirationDate.Value.ToString("yyyy-MM-dd"));
        //    if (apikey != null) parameters.Add("key", apikey);

        //    var queryString = "?" + string.Join("&",
        //        Array.ConvertAll(
        //        parameters.AllKeys, key =>
        //            string.Format(
        //            "{0}={1}",
        //            HttpUtility.UrlEncode(key),
        //            HttpUtility.UrlEncode(parameters[key])
        //            )));
        //    return uri + queryString;
        //}
        /// <summary>
        /// Constructs a search query using the builder pattern
        /// </summary>
    private class SearchBuilder
    {
        //required
        private string apikey;
        private string url;


        private CenterdActivity? activity = null;
        private Capability? capability = null;
        private DateTime? expirationDate = null;
        private string location = null;
        private string query = null;
        private double? distance = null;
        private int? startIndex = null;
        private int? pageSize = null;

        
        public SearchBuilder(string apikey, string url)
        {
            
        }
         public SearchBuilder Activity (CenterdActivity value)
         {
             activity = value;
             return this;
         }
         public SearchBuilder Capability  (Capability value)
         {
             capability = value;
             return this;
         }
         public SearchBuilder ExpirationDate  (DateTime value)
         {
             expirationDate = value;
             return this;
         }
         public SearchBuilder Location (String value)
         {
             location = value;
             return this;
         }
         public SearchBuilder Query (string value)
         {
             query = value;
             return this;
         }
         public SearchBuilder Distance (double? value)
         {
             distance = value;
             return this;
         }
         public SearchBuilder StartIndex (int? value)
         {
             startIndex = value;
             return this;
         }
         public   SearchBuilder PageSize (int? value)
         {
             pageSize = value;
             return this;
         }

        public string Create()
        {
            NameValueCollection parameters = new NameValueCollection();

            if (location != null) parameters.Add("l", location);
            if (query != null) parameters.Add("q", query);
            if (distance.HasValue) parameters.Add("d", distance.ToString());
            if (startIndex.HasValue) parameters.Add("si", startIndex.ToString());
            if (pageSize.HasValue) parameters.Add("ps", pageSize.ToString());
            if (activity.HasValue) parameters.Add("a", ((int)activity).ToString());
            if (capability.HasValue) parameters.Add("c", ((int)capability).ToString());
            if (expirationDate.HasValue) parameters.Add("ed", expirationDate.Value.ToString("yyyy-MM-dd"));
            if (apikey != null) parameters.Add("key", apikey);

            var queryString = "?" + string.Join("&",
                Array.ConvertAll(
                parameters.AllKeys, key =>
                    string.Format(
                    "{0}={1}",
                    HttpUtility.UrlEncode(key),
                    HttpUtility.UrlEncode(parameters[key])
                    )));
            return url + queryString;
        }
    }
    
    }
}
